
import java.util.*;
import java.io.*;


/**
 *  A simple template driven parser for HTML pages.
 * [TBD - explain template syntax]
 * [TBD] http://developer.apple.com/technotes/tn/tn1141.html
 * [TBD] http://www.apple.com/sherlock/plugindev.html
http://www.apple-donuts.com/
 */

public class PageParser
{
    /* Internal representation of an @(type[=binding]) PageParser
     * template expression.
     */
    private static class MatchAny {
	final String binding;

	MatchAny(String s) {
	    int i = s.indexOf('=');
	    binding = (i != -1) ? s.substring(i+1) : null;
	}

 	public String toString() {
	    String s = (binding != null) ? "=" + binding : "";
	    return "@(any" + s + ")";
	}
    }

    private final Object whitespace = new String("whitespace");

    /* Template contains three kinds of elements:
     * 1 - A string which must match whatever appears 
     *   in the input stream next.
     * 2 - The special constant whitespace which matches 
     *   an arbitrary sequence of whitespace characters.
     * 3 - A Match object that corresponds to an 
     *   @(type[=binding]) expression.
     */
    private final ArrayList template;


    public PageParser(String s) 
    {
	ArrayList template = new ArrayList();

	/* Watch out - index i is modified within the body
	 * of this for loop.
	 */
	for(int i = 0; i < s.length(); i++) { 
	    char c = s.charAt(i);

	    /* If c is a white space character add a 
	     * whitespace token to the template and 
	     * skip any additional white space.
	     */
	    if (Character.isWhitespace(c)) {
		template.add(whitespace);
		while(i + 1 < s.length()) {
		    if (!Character.isWhitespace(s.charAt(i + 1))) {
			break;
		    }
		    else {
			i += 1;
		    }
		}
	    }
	    /* If c is an '@' then consume a "@(type[=binding])"
	     * expression and add a Match object to the template.
	     */
	    else if (c == '@') {
		int i0 = s.indexOf('(', i);
		int i1 = s.indexOf(')', i);
		if ((i0 == -1) || (i1 == -1) || (i0 != i + 1)) {
		    throw new IllegalArgumentException("bad template");
		}
		String expr = s.substring(i0+1, i1);
		if (expr.startsWith("any")) {
		    template.add(new MatchAny(expr));
		}
		i = i1;
	    }
	    /* Otherwise c is the beginning of a literal so
	     * collect characters until hit whitespace or
	     * an '@'; append the literal to the template.
	     */
	    else {
		StringBuffer sb = new StringBuffer();
		sb.append(c);
		while(i + 1 < s.length()) {
		    char l = s.charAt(i + 1);
		    if (l == '@' || Character.isWhitespace(l)) {
			break;
		    }
		    else {
			sb.append(l);
			i += 1;
		    }
		}
		template.add(sb.toString());
	    }
	}

	/* Verify that each @(any) is followed by a literal 
	 */
	ListIterator templateIterator = template.listIterator();
	while(templateIterator.hasNext()) {
	    Object token = templateIterator.next();
	    if (token instanceof MatchAny) {
		if (!templateIterator.hasNext()) {
		    throw new IllegalArgumentException("template can't end with @(any)");
		}
		Object nextToken = templateIterator.next();
		if (!(nextToken instanceof String)) {
		    throw new IllegalArgumentException("@(any) must be followed by a literal");
		}
	    }
	}

	this.template = template;
    }


    /** 
     * Return one character from the Reader. If we've been interrupted, close 
     * the Reader and then barge on ahead with the read so that an IOException 
     * is thrown.
     */
    private static char readChar(Reader r) throws IOException {
	if (Thread.currentThread().isInterrupted()) {
	    r.close();
	}
	char[] c = new char[1];
	if (r.read(c, 0, 1) != 1) {
	    throw new EOFException();
	}
	return c[0];
    }


    private static void skipWhitespace(PushbackReader r) throws IOException {
	char c;
	do {
	    c = readChar(r);
	}
	while(Character.isWhitespace(c));
	r.unread(c);
    }


    /* Consume characters from the Reader until a match for the template has
     * been found.  The values for any of the '@(any=binding)' elements of 
     * template are stored in the Map.  If EOF is reached before a complete
     * match has been found return false, otherwise return true.
     */
    public boolean parse(Reader r, Map m)
    {
	PushbackReader inputReader = new PushbackReader(r, 1);

	while(true) {
	    ListIterator templateIterator = template.listIterator();
	    try {
		while(templateIterator.hasNext()) {
		    Object token = templateIterator.next();
		    
		    if (token == whitespace) {
			skipWhitespace(inputReader);
		    }
		    /* Match a literal exactly or abort this attempt to match
		     * the template (break out of the the templateIterator while loop).
		     */
		    else if (token instanceof String) {
			String tokenString = (String)token;
			boolean match = true;
			char c;
			for(int j = 0; match && (j < tokenString.length()); j++) {
			    match = ((c = readChar(inputReader)) == tokenString.charAt(j));
			}
			if (!match) {
			    break;
			}
		    }
		    /* Skip characters in the input stream until we find an 
		     * occurrence of the next literal token in the template.
		     */
		    else if (token instanceof MatchAny) {
			Object binding = ((MatchAny)token).binding;
			String nextLiteral = (String)(templateIterator.next());
			StringBuffer match = new StringBuffer();

			if (nextLiteral == whitespace) {
			    char c;
			    while(!(Character.isWhitespace(c = readChar(inputReader)))) {
				match.append(c);
			    }
			    if (binding != null) {
				m.put(binding, match.toString());
			    }
			    skipWhitespace(inputReader);
			}
			else {
			    int n;
			    while((n = match.toString().indexOf(nextLiteral)) == -1) {
				match.append(readChar(inputReader));
			    }
			    if (binding != null) {
				m.put(binding, match.toString().substring(0, n));
			    }
			}
		    }
		}
	    }
	    catch (IOException e) {
		return false;
	    }
	
	    /* We've matched the entire template */
	    if (!templateIterator.hasNext()) { 
		return true;
	    }
	}
    }


    public String toString() {
	return template.toString();
    }
}


